#include "particle_sys.h"

using namespace G3;

//==============================================================================
// Only for tolua bindings
//==============================================================================
int G3PS_NumMaxParticles = 100;
int G3PS_NumToRelease = 1;
float G3PS_ReleaseInterval = 0.01f;
float G3PS_LifeCycle = 1.0f;
float G3PS_Size = 32.0f;
float G3PS_Mass = 1.0f;
COL4F G3PS_Color;
VEC3F G3PS_Position;
VEC3F G3PS_Velocity (0, 1, 0);
VEC3F G3PS_Gravity (0, -1, 0);
bool G3PS_AirResistence = true;
float G3PS_VelocityVar = 1.0f;
TEXTURE_FILTER G3PS_Filter = G3::GTF_MIPMAP;
std::string G3PS_BitmapFile;

//==============================================================================
// Constructors
//==============================================================================
G3::COLL_MATERIAL::COLL_MATERIAL () {
	Elasticity = 0.0f;
	Friction = 0.5f;
	Type = GCT_BOUNCE;
}

G3::COLL_POLYGON3F::COLL_POLYGON3F () {
	MaterialID = 0;
}

G3::COLL_MESH::COLL_MESH () {
	// NOP - No OPeration
}

G3::PARTICLE::PARTICLE () {
	Birth = 0.0f;
	Mass = 1.0f;
	Stuck = false;
}

G3::PARTICLE_SYSTEM::PARTICLE_SYSTEM () {
	CollGeom = NULL;

	NumMaxParticles = 1;
	NumToRelease = 1;
	ReleaseInterval = 0.01f;
	LifeCycle = 1.0f;
	Size = 32.0f;
	Mass = 1.0f;
	AirResistence = true;
	VelocityVar = 1.0f;

	Log = &MainLog;

	CurTime = 0.0f;
	LastUpdate = 0.0f;
	MaxPointSize = 1.0f;
}

//==============================================================================
// Destructors
//==============================================================================
G3::COLL_MATERIAL::~COLL_MATERIAL () {
	// __asm ("nop");
}

G3::COLL_POLYGON3F::~COLL_POLYGON3F () {
	Clear ();
}

G3::COLL_MESH::~COLL_MESH () {
	Clear ();
}

G3::PARTICLE_SYSTEM::~PARTICLE_SYSTEM () {
	Clear ();
}

//==============================================================================
// = operators
//==============================================================================
COLL_POLYGON3F& G3::COLL_POLYGON3F::operator= (const COLL_POLYGON3F &cp) {
	Vertices = cp.Vertices;
	Plane = cp.Plane;
	MaterialID = cp.MaterialID;

	return *this;
}

PARTICLE& G3::PARTICLE::operator= (const PARTICLE &p) {
	Position = p.Position;
	Velocity = p.Velocity;
	Mass = p.Mass;
	Birth = p.Birth;
	Stuck = p.Stuck;

	return *this;
}

//==============================================================================
// Clear functions
//==============================================================================
void G3::COLL_POLYGON3F::Clear () {
	Vertices.clear ();
}

void G3::COLL_MESH::Clear () {
	for (int i=0; i<(int) Polygons.size (); i++)
		Polygons [i].Clear ();

	Polygons.clear ();
	Materials.clear ();
	Vertices.clear ();
}

void G3::PARTICLE_SYSTEM::Clear () {
	ActiveParticles.clear ();
	FreeParticles.clear ();
}

//==============================================================================
// Gets a POLYGON3X from the COLL_POLYGON3X class
//==============================================================================
G3::POLYGON3F G3::COLL_POLYGON3F::GetPolygon (std::vector<VEC3F> *aVertices) {
	G3::POLYGON3F p;

	Assert (aVertices, "COLL_POLYGON3F::GetPolygon: Vertices argument is a NULL pointer");

	for (int i=0; i<(int) Vertices.size (); i++) {
		Assert (aVertices->size () >= Vertices [i], "COLL_POLYGON3F::GetPolygon: Vertices argument is invalid");

		p.Vertices.push_back ((*aVertices) [Vertices [i]]);
	}

	p.Plane = Plane;

	return p;
}

//==============================================================================
// Recalculates planes in the collision mesh
//==============================================================================
void G3::COLL_MESH::CalcPlanes () {
	for (int i=0; i<(int) Polygons.size (); i++) {
		Polygons [i].Plane = PLANE3F (Vertices [Polygons [i].Vertices [0]],
									  Vertices [Polygons [i].Vertices [1]],
									  Vertices [Polygons [i].Vertices [2]]);
	}
}

void G3::COLL_MESH::Render () {
    int i, j;

    glDisable (GL_TEXTURE_2D);

    glColor4f (0.7f, 0.7f, 0.7f, 1.0f);

    for (i=0; i<(int)Polygons.size (); i++) {
        glBegin (GL_POLYGON);
            for (j=0; j<(int)Polygons [i].Vertices.size (); j++) {
                glVertex3f (Vertices [Polygons [i].Vertices [j]].x, Vertices [Polygons [i].Vertices [j]].y, Vertices [Polygons [i].Vertices [j]].z);
            }
        glEnd ();
    }
}

//==============================================================================
// Initializes the particle system
//
//	TODO:: Software mode, Cg Vertex Shader mode
//! \todo Software mode, Cg Vertex Shader mode
//==============================================================================
bool G3::PARTICLE_SYSTEM::Init () {
	if (GLEE_ARB_point_parameters) {
		glGetFloatv (GL_POINT_SIZE_MAX_ARB, &MaxPointSize);

		if (Size > MaxPointSize)
			Size = MaxPointSize;

		return true;
	} else {
		// TODO:: Use software particle drawing
		//! \todo Use software particle drawing
		if (Log)
			Log->Report ("WARNING! PARTICLE_SYSTEM::Init: GL_ARB_point_parameters not supported..\n");
	}
	return false;
}

//==============================================================================
// Loads the particle system from a stream
//==============================================================================
bool G3::PARTICLE_SYSTEM::Load (STREAM *aStream) {
	bool Result = false;
	TEXTURE_FILTER LastFilter = TextureManager.used_filter;

	Assert (aStream, "PARTICLE_SYSTEM::Load: Stream argument is a NULL pointer");

	tolua_particlescript_open (MainLuaHandler.LuaState);

	if (MainLuaHandler.Execute (aStream) == -1) {
		printf ("Exec failed\n");
		return false;
	}

	NumMaxParticles = G3PS_NumMaxParticles;
	NumToRelease = G3PS_NumToRelease;
	ReleaseInterval = G3PS_ReleaseInterval;
	LifeCycle = G3PS_LifeCycle;
	Size = G3PS_Size;
	Mass = G3PS_Mass;
	Color = G3PS_Color;
	Position = G3PS_Position;
	Velocity = G3PS_Velocity;
	Gravity = G3PS_Gravity;
	AirResistence = G3PS_AirResistence;
	VelocityVar = G3PS_VelocityVar;

	TextureManager.used_filter = G3PS_Filter;

	Texture = TextureManager.GetHandle (G3PS_BitmapFile.c_str ());

	TextureManager.used_filter = LastFilter;

	return Result;
}

//==============================================================================
// Loads the particle system from a file
//==============================================================================
bool G3::PARTICLE_SYSTEM::Load (const char *aFilename) {
	Assert (aFilename, "PARTICLE_SYSTEM::Load: Filename argument is a NULL pointer");

	string Path = MainFilesys.GetPath (aFilename);

	if (Path.empty()) {
	    if (Log)
            Log->Report("ERROR! Could not find particle system script \"%s\"..\n", aFilename);

	    return false;
	}

	// Particlescripts are small - no need to stream them
	return Load (MainFileHandler.ReadWholeFile (Path.c_str ()));
}

void G3::PARTICLE_SYSTEM::Emit() {
   	std::list<PARTICLE>::iterator it;
    PARTICLE *p = NULL;
    int i = 0;

    // Respawn dead / undead particles
    for (i=0; i<(int)NumToRelease; i++) {
        // RIP-ed ?
        if (!FreeParticles.empty ()) {
            it = ActiveParticles.insert (ActiveParticles.end (), *FreeParticles.begin ());
            FreeParticles.erase (FreeParticles.begin ());
        // Unborn ?
        } else {
            if (NumMaxParticles < 0.0f || ActiveParticles.size () < NumMaxParticles) {
                p = new PARTICLE;

                if (!p) {
                    NumMaxParticles = ActiveParticles.size ();
                    if (Log)
                        Log->Report ("WARNING! PARTICLE_SYSTEM::Update: Not enough memory for another particle..\n");
                    return;
                }

                it = ActiveParticles.insert (ActiveParticles.end (), *p);
            }
        }

        // Give it a purpose
        if (NumMaxParticles < 0.0f || ActiveParticles.size () < NumMaxParticles) {
            it->Velocity = Velocity;

            if (VelocityVar != 0.0f)
                it->Velocity += GetRandVector3<float> () * VelocityVar;

            it->Birth = CurTime;
            it->Position = Position;
        }
    }
}

//==============================================================================
// Updates the particle system
//==============================================================================
void G3::PARTICLE_SYSTEM::Update (float aElapsedTime) {
    std::list<PARTICLE>::iterator it;
	INTERSECTION3F I;
	VEC3F OldPos, Vn;
	int i = 0;

	CurTime += aElapsedTime;
	// Update active particles
	it = ActiveParticles.begin ();
	while (it != ActiveParticles.end ()) {
		// Particle is D.E.D (dead) ?
		if (LifeCycle > 0.0f && CurTime - it->Birth >= LifeCycle) {
			// A ghost can't be stuck anymore
			it->Stuck = false;
			it->Mass = Mass;
			FreeParticles.insert (FreeParticles.end (), *it);
			it = ActiveParticles.erase (it);
		} else {
			// Avoid updating stuck particles
			if (it->Stuck) {
				it++;
				continue;
			}

			if (it->Mass != 0.0f) {
				// Add gravity
				it->Velocity += Gravity * aElapsedTime * it->Mass;
				// Add wind
				if (AirResistence)
					it->Velocity += (Wind - it->Velocity) * aElapsedTime / it->Mass;
			} else {
				// Add wind
				if (AirResistence)
					it->Velocity += (Wind - it->Velocity) * aElapsedTime;
			}

			// Move the particle
			OldPos = it->Position;
			it->Position += it->Velocity * aElapsedTime;

			// Particle Collision-Detection
			if (CollGeom) {
				// TODO:: Do an ABT / Octree / Kd-tree
				//! \todo Do an ABT / Octree / Kd-tree on the collision geometry
				// TODO:: Optimize heavily
				//! \todo Optimize heavily
				for (i=0; i<(int) CollGeom->Polygons.size (); i++) {
					// Went through the plane?
					if (CollGeom->Polygons [i].Plane.ClassifyPoint (it->Position) == G3::NEGATIVE) {
						Assert (CollGeom->Vertices.size () >= CollGeom->Polygons [i].Vertices [0] &&
								CollGeom->Vertices.size () >= CollGeom->Polygons [i].Vertices [1] &&
								CollGeom->Vertices.size () >= CollGeom->Polygons [i].Vertices [2],
								"PARTICLE_SYSTEM::Update: CollisionGeometry has invalid polygons");
						// Through the triangle?
						//! \bug Some particles phase through the triangles
						I = RayTriIntersection (CollGeom->Polygons [i].Plane,
												CollGeom->Vertices [CollGeom->Polygons [i].Vertices [0]],
												CollGeom->Vertices [CollGeom->Polygons [i].Vertices [1]],
												CollGeom->Vertices [CollGeom->Polygons [i].Vertices [2]],
												RAY3F (OldPos, it->Position));
						// That's right
						if (G3_TESTFLAG (I.Flags, I.INT_POLY)) {
							Assert (CollGeom->Materials.size () >= CollGeom->Polygons [i].MaterialID, "PARTICLE_SYSTEM::Update: CollisionGeometry has invalid polygon MaterialIDs");

							it->Color = G3::COL4F (1.0f, 0, 0, 1.0f);
							// What was that triangle made of?
							switch (CollGeom->Materials [CollGeom->Polygons [i].MaterialID].Type) {
								// Elastic material
								case GCT_BOUNCE:
									it->Position = I.Point + CollGeom->Polygons [i].Plane.n * 2 * G3_EPSILON;

									Vn = CollGeom->Polygons [i].Plane.n * (CollGeom->Polygons [i].Plane.n * it->Velocity);
									it->Velocity = it->Velocity - Vn - Vn * CollGeom->Materials [CollGeom->Polygons [i].MaterialID].Elasticity;
									break;
								// Of sth that absorbs these particles
								case GCT_RECYCLE:
									it->Birth -= LifeCycle;
									break;
								// Or of something sticky
								case GCT_STICK:
									it->Position = I.Point + CollGeom->Polygons [i].Plane.n * 2 * G3_EPSILON;
									it->Velocity.Reset ();
									it->Stuck = true;
									break;
								default: break;
							}
						} else {
                            it->Color = Color;
						}
					}
				}
			}
		}
		it++;
	}

	// Emit fresh particles ?
	if (ReleaseInterval > 0.0f && CurTime - LastUpdate > ReleaseInterval) {
		LastUpdate = CurTime;

		Emit();
	}
}

//==============================================================================
// Renders the particle system
//==============================================================================
void G3::PARTICLE_SYSTEM::Render () {
	std::list<PARTICLE>::iterator it = ActiveParticles.begin ();
    // This is how our point sprite's size will be modified by
    // distance from the viewer.
	// 1/(a + b*d + c*d^2)
    float quadratic[] =  { 1.0f, 0.0f, 0.01f };
    glPointParameterfvARB (GL_POINT_DISTANCE_ATTENUATION_ARB, quadratic);

    // The alpha of a point is calculated to allow the fading of points
    // instead of shrinking them past a defined threshold size. The threshold
    // is defined by GL_POINT_FADE_THRESHOLD_SIZE_ARB and is not clamped to
	// the minimum and maximum point sizes.
    glPointParameterfARB (GL_POINT_FADE_THRESHOLD_SIZE_ARB, 60.0f);

    glPointParameterfARB (GL_POINT_SIZE_MIN_ARB, 1.0f);
    glPointParameterfARB (GL_POINT_SIZE_MAX_ARB, MaxPointSize);

    // Specify point sprite texture coordinate replacement mode for each texture unit
    glTexEnvf (GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE);

    glEnable (GL_DEPTH_TEST);
    glDepthMask (GL_FALSE);

	glEnable (GL_BLEND);
	glBlendFunc (GL_SRC_ALPHA, GL_ONE);

	glEnable (GL_TEXTURE_2D);

	TextureManager.Apply (Texture);

    glEnable (GL_POINT_SPRITE_ARB);

    glPointSize (Size);
	//glColor4f (Color.r, Color.g, Color.b, Color.a);

	// TODO:: OpenGL Vertex Arrays
	//! \todo Use dynamic VBO for the particles
	glBegin (GL_POINTS);
		while (it != ActiveParticles.end ()) {
		    glColor4f (it->Color.r, it->Color.g, it->Color.b, it->Color.a);
			glVertex3f (it->Position.x, it->Position.y, it->Position.z);

			it++;
		}
	glEnd ();

	glDisable (GL_POINT_SPRITE_ARB);

    glDepthMask (GL_TRUE);
    glDisable (GL_BLEND);
}

//==============================================================================
// Restarts the particle system
//==============================================================================
void G3::PARTICLE_SYSTEM::Restart () {
	std::list<PARTICLE>::iterator it = ActiveParticles.begin ();

	while (it != ActiveParticles.end ()) {
		FreeParticles.insert (FreeParticles.end (), *it);
		it = ActiveParticles.erase (it);

		it++;
	}
}

//==============================================================================
// Dumps a single particle into the log
//==============================================================================
void G3::PARTICLE::Dump (LOG *aLog) {
	if (aLog) {
		aLog->Report (" Dumping Particle:\n");

		aLog->Report ("  Position: (%f, %f, %f)\n", Position.x, Position.y, Position.z);
		aLog->Report ("  Velocity: (%f, %f, %f)\n", Velocity.x, Velocity.y, Velocity.z);
		aLog->Report ("  Stuck: %d\n", Stuck);
		aLog->Report ("  Mass: %f\n", Mass);
		aLog->Report ("  Birth: %f\n", Birth);
	}
}

//==============================================================================
// Dumps the particle system into the log
//==============================================================================
void G3::PARTICLE_SYSTEM::Dump (G3::LOG *aLog) {
	std::list<PARTICLE>::iterator it = ActiveParticles.begin ();
	if (aLog) {
		aLog->Report ("Dumping Particle system:\n");

		aLog->Report (" Collision Geometry: 0x%X\n", CollGeom);
		aLog->Report (" NumMaxParticles: %d\n", NumMaxParticles);
		aLog->Report (" NumToRelease: %d\n", NumToRelease);
		aLog->Report (" ReleaseInterval: %f\n", ReleaseInterval);
		aLog->Report (" LifeCycle: %f\n", LifeCycle);
		aLog->Report (" Size: %f\n", Size);
		aLog->Report (" Mass: %f\n", Mass);
		aLog->Report (" Color: (%f, %f, %f, %f)\n", Color.r, Color.g, Color.b, Color.a);
		aLog->Report (" Position: (%f, %f, %f)\n", Position.x, Position.y, Position.z);
		aLog->Report (" Velocity: (%f, %f, %f)\n", Velocity.x, Velocity.y, Velocity.z);
		aLog->Report (" Gravity: (%f, %f, %f)\n", Gravity.x, Gravity.y, Gravity.z);
		aLog->Report (" Wind: (%f, %f, %f)\n", Wind.x, Wind.y, Wind.z);
		aLog->Report (" AirResistence: %d\n", AirResistence);
		aLog->Report (" VelocityVar: %f\n", VelocityVar);
		aLog->Report (" Texture: 0x%X\n\n", (uint) Texture);

		while (it != ActiveParticles.end ()) {
			it->Dump (aLog);

			it++;
		}
	}
}

